<?php

class Db_Adapter_Mongo extends DbNosql
{
	/**
	 * current query result
	 * @var result
	 */
	protected $_result = NULL;

	/**
	 * @var MongoDB object
	 */
	protected $_db = NULL;

	/**
	 * @var MongoConnection Object
	 */
	protected $_connection = NULL;

	/**
	 * @var bool
	 */
	protected $_connected = FALSE;

	/**
	 * @var BOOL
	 */
	protected $_profiling = FALSE;

	/**
	 * check and validate setup config be
	 * @param array $config
	 * @return array validated configuration array
	 */
	protected function _validateConfig($config)
	{
		$config = array_merge(array(
			'hostnames' => 'locahost:27017',
			'username' => '',
			'password' => '',
			'dbname' => 'agilet',
			'options' => array()
		), $config);

		$config['options']['connect'] = FALSE;

		$config['connection'] = sprintf('mongodb://%s:%s@%s/%s', $config['username'], $config['password'], $config['hostnames'], $config['dbname']);

		return $config;
	}

	/**
	 * global configuration.
	 * @link
	 * http://docs.mongodb.org/manual/tutorial/install-mongodb-on-redhat-centos-or-fedora-linux/
	 * @throws Exeception
	 */
	protected function __construct($name)
	{

		$config = Agilet::loadConfig('mongo.php', $name);

		$this -> _config = $this -> _validateConfig($config);

		$this -> _name = $name;

		self::$_inst[$name] = $this;
	}

	/**
	 * All commands for which benchmarking could be useful
	 * are executed by this method
	 *
	 * This allows for easy benchmarking
	 */
	public function _call($command, array $arguments = array(), array $values = NULL)
	{
		$this -> _connected OR $this -> connect();

		extract($arguments);

		if (!empty($this -> _config['profiling']))
		{
			$_bm_name = isset($collection_name) ? $collection_name . '.' . $command : $command;

			if (isset($query))
				$_bm_name .= ' (' . json_encode($query) . ')';
			if (isset($criteria))
				$_bm_name .= ' (' . json_encode($criteria) . ')';
			if (isset($values))
				$_bm_name .= ' (' . json_encode($values) . ')';

			// $_bm = Profiler::start("MangoDB {$this->_name}", $_bm_name);
		}

		if (isset($collection_name))
		{
			$c = $this -> _db -> selectCollection($collection_name);
		}

		switch ( $command) {
			case 'ensureIndex' :
				$r = $c -> ensureIndex($keys, $options);
				break;
			case 'createCollection' :
				$r = $this -> _db -> createCollection($name, $capped, $size, $max);
				break;
			case 'dropCollection' :
				$r = $this -> _db -> dropCollection($name);
				break;
			case 'command' :
				$r = $this -> _db -> command($values);
				break;
			case 'execute' :
				$r = $this -> _db -> execute($code, $args);
				break;
			case 'batchInsert' :
				$r = $c -> batchInsert($values, $options);
				break;
			case 'count' :
				$r = $c -> count($query);
				break;
			case 'findOne' :
				$r = $c -> findOne($query, $fields);
				break;
			case 'find' :
				$r = $c -> find($query, $fields);
				break;
			case 'group' :
				$r = $c -> group($keys, $initial, $reduce, $condition);
				break;
			case 'update' :
				$r = $c -> update($criteria, $values, $options);
				break;
			case 'insert' :
				$r = $c -> insert($values, $options);
				break;
			case 'remove' :
				$r = $c -> remove($criteria, $options);
				break;
			case 'save' :
				$r = $c -> save($values, $options);
				break;
			case 'getFile' :
				$r = $this -> gridFS() -> findOne($criteria);
				break;
			case 'getFiles' :
				$r = $this -> gridFS() -> find($query, $fields);
				break;
			case 'setFileBytes' :
				$r = $this -> gridFS() -> storeBytes($bytes, $extra, $options);
				break;
			case 'setFile' :
				$r = $this -> gridFS() -> storeFile($filename, $extra, $options);
				break;
			case 'removeFile' :
				$r = $this -> gridFS() -> remove($criteria, $options);
				break;
		}

		if (isset($_bm))
		{
			// Profiler::stop($_bm);
		}

		return $r;
	}

	/**
	 * @return bool
	 */
	public function connect()
	{
		if ($this -> _db)
		{
			return TRUE;
		}
		try
		{
			$config = $this -> _config;

			$this -> _connection = new Mongo($config['connection']);

			$this -> _db = $this -> _connection -> selectDB($config['dbname']);
		}
		catch(Exception $e)
		{
			throw new Agilet_Exception("could not connect to mongodb " . $e -> getMessage());
		}

		return $this -> _connected = TRUE;

	}

	public function disconnect()
	{
		if ($this -> _connection)
		{
			$this -> _connection -> close();
		}

		$this -> _db = $this -> _connection = $this -> _connected = NULL;
	}

	/** Database Management */

	public function lastError()
	{
		return $this -> _connected ? $this -> _db -> lastError() : NULL;
	}

	public function prevError()
	{
		return $this -> _connected ? $this -> _db -> prevError() : NULL;
	}

	public function resetError()
	{
		return $this -> _connected ? $this -> _db -> resetError() : NULL;
	}

	public function command(array $data)
	{
		return $this -> _call('command', array(), $data);
	}

	public function execute($code, array $args = array())
	{
		return $this -> _call('execute', array(
			'code' => $code,
			'args' => $args
		));
	}

	public function db()
	{
		return $this -> _connected ? $this -> _db : FALSE;
	}

	/** Collection management */

	public function createCollection($name, $capped = FALSE, $size = 0, $max = 0)
	{
		return $this -> _call('createCollection', array(
			'name' => $name,
			'capped' => $capped,
			'size' => $size,
			'max' => $max
		));
	}

	public function dropCollection($name)
	{
		return $this -> _call('dropCollection', array('name' => $name));
	}

	public function ensureIndex($collection_name, $keys, $options = array())
	{
		return $this -> _call('ensureIndex', array(
			'collection_name' => $collection_name,
			'keys' => $keys,
			'options' => $options
		));
	}

	/** Data Management */

	public function batchInsert($collection_name, array $a, array $options = array())
	{
		return $this -> _call('batchInsert', array(
			'collection_name' => $collection_name,
			'options' => $options
		), $a);
	}

	public function count($collection_name, array $query = array())
	{
		return $this -> _call('count', array(
			'collection_name' => $collection_name,
			'query' => $query
		));
	}

	public function findOne($collection_name, array $query = array(), array $fields = array())
	{
		return $this -> _call('findOne', array(
			'collection_name' => $collection_name,
			'query' => $query,
			'fields' => $fields
		));
	}

	public function find($collection_name, array $query = array(), array $fields = array())
	{
		return $this -> _call('find', array(
			'collection_name' => $collection_name,
			'query' => $query,
			'fields' => $fields
		));
	}

	public function group($collection_name, $keys, array $initial, $reduce, array $condition = array())
	{
		return $this -> _call('group', array(
			'collection_name' => $collection_name,
			'keys' => $keys,
			'initial' => $initial,
			'reduce' => $reduce,
			'condition' => $condition
		));
	}

	public function update($collection_name, array $criteria, array $newObj, $options = array())
	{
		return $this -> _call('update', array(
			'collection_name' => $collection_name,
			'criteria' => $criteria,
			'options' => $options
		), $newObj);
	}

	public function insert($collection_name, array $a, $options = array())
	{
		return $this -> _call('insert', array(
			'collection_name' => $collection_name,
			'options' => $options
		), $a);
	}

	public function remove($collection_name, array $criteria, $options = array())
	{
		return $this -> _call('remove', array(
			'collection_name' => $collection_name,
			'criteria' => $criteria,
			'options' => $options
		));
	}

	public function save($collection_name, array $a, $options = array())
	{
		return $this -> _call('save', array(
			'collection_name' => $collection_name,
			'options' => $options
		), $a);
	}

	/** File management */

	public function gridFS($arg1 = NULL, $arg2 = NULL)
	{
		$this -> _connected OR $this -> connect();

		if (!isset($arg1))
		{
			$arg1 = isset($this -> _config['gridFS']['arg1']) ? $this -> _config['gridFS']['arg1'] : 'fs';
		}

		if (!isset($arg2) && isset($this -> _config['gridFS']['arg2']))
		{
			$arg2 = $this -> _config['gridFS']['arg2'];
		}

		return $this -> _db -> getGridFS($arg1, $arg2);
	}

	public function getFile(array $criteria = array())
	{
		return $this -> _call('getFfile', array('criteria' => $criteria));
	}

	public function getFiles(array $query = array(), array $fields = array())
	{
		return $this -> _call('getFiles', array(
			'query' => $query,
			'fields' => $fields
		));
	}

	public function setFileBytes($bytes, array $extra = array(), array $options = array())
	{
		return $this -> _call('setFileBytes', array(
			'bytes' => $bytes,
			'extra' => $extra,
			'options' => $options
		));
	}

	public function setFile($filename, array $extra = array(), array $options = array())
	{
		return $this -> _call('setFile', array(
			'filename' => $filename,
			'extra' => $extra,
			'options' => $options
		));
	}

	public function removeFile(array $criteria = array(), array $options = array())
	{
		return $this -> _call('removeFile', array(
			'criteria' => $criteria,
			'options' => $options
		));
	}

	/**
	 * Get the next auto-increment value for the given key
	 *
	 * @param string $key
	 * @param string $collection
	 * @return int
	 * @throws MongoException
	 */
	public function getAutoIncrement($collection, $sequence_collection = 'autoincrement')
	{
		$data = $this -> _db -> command(array(
			'findAndModify' => $sequence_collection,
			'query' => array('_id' => $collection),
			'update' => array('$inc' => array('num' => 1)),
			'upsert' => TRUE,
			'new' => TRUE,
		));
						
		return $data['value']['num'];
	}

	/**
	 * create uuid string to stored as mongo id
	 * @param  string/int       $kind_id
	 * @param  number           $node
	 * @return int
	 */
	public function createUuid($collection, $node_id = null)
	{
		static $kinds = array(
			'users' => 1,
			'comments' => 2
		);

		$time = microtime(1) * 10000;

		$kind_id = $kinds[$collection];

		if (null == $node_id)
		{
			$node_id = mt_rand(0, PHP_INT_MAX);
		}
		return array_pop(unpack("H*hex", pack("NnnN", $time, intval($kind_id), $time<<32, $node_id)));
	}

}
